/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "fileapidataextractor.h"

#include "cmakeprojectnodes.h"
#include "projecttreehelper.h"

#include <projectexplorer/projectnodes.h>

#include <utils/algorithm.h>
#include <utils/qtcassert.h>
#include <utils/qtcprocess.h>

#include <QDir>

using namespace ProjectExplorer;
using namespace Utils;

namespace {

using namespace CMakeProjectManager;
using namespace CMakeProjectManager::Internal;
using namespace CMakeProjectManager::Internal::FileApiDetails;

// --------------------------------------------------------------------
// Helpers:
// --------------------------------------------------------------------

class CMakeFileResult
{
public:
    QSet<FilePath> cmakeFiles;

    std::vector<std::unique_ptr<ProjectExplorer::FileNode>> cmakeNodesSource;
    std::vector<std::unique_ptr<ProjectExplorer::FileNode>> cmakeNodesBuild;
    std::vector<std::unique_ptr<ProjectExplorer::FileNode>> cmakeNodesOther;
    std::vector<std::unique_ptr<ProjectExplorer::FileNode>> cmakeListNodes;
};

CMakeFileResult extractCMakeFilesData(const std::vector<FileApiDetails::CMakeFileInfo> &cmakefiles,
                                      const FilePath &sourceDirectory,
                                      const FilePath &buildDirectory)
{
    CMakeFileResult result;

    QDir sourceDir(sourceDirectory.toString());
    QDir buildDir(buildDirectory.toString());

    for (const CMakeFileInfo &info : cmakefiles) {
        const FilePath sfn = FilePath::fromString(
            QDir::cleanPath(sourceDir.absoluteFilePath(info.path)));
        const int oldCount = result.cmakeFiles.count();
        result.cmakeFiles.insert(sfn);
        if (oldCount < result.cmakeFiles.count()) {
            if (info.isCMake && !info.isCMakeListsDotTxt) {
                // Skip files that cmake considers to be part of the installation -- but include
                // CMakeLists.txt files. This unbreaks cmake binaries running from their own
                // build directory.
                continue;
            }

            auto node = std::make_unique<FileNode>(sfn, FileType::Project);
            node->setIsGenerated(info.isGenerated
                                 && !info.isCMakeListsDotTxt); // CMakeLists.txt are never
                                                               // generated, independent
                                                               // what cmake thinks:-)

            if (info.isCMakeListsDotTxt) {
                result.cmakeListNodes.emplace_back(std::move(node));
            } else if (sfn.isChildOf(sourceDir)) {
                result.cmakeNodesSource.emplace_back(std::move(node));
            } else if (sfn.isChildOf(buildDir)) {
                result.cmakeNodesBuild.emplace_back(std::move(node));
            } else {
                result.cmakeNodesOther.emplace_back(std::move(node));
            }
        }
    }

    return result;
}

Configuration extractConfiguration(std::vector<Configuration> &codemodel, QString &errorMessage)
{
    if (codemodel.size() == 0) {
        qWarning() << "No configuration found!";
        errorMessage = "No configuration found!";
        return {};
    }
    if (codemodel.size() > 1)
        qWarning() << "Multi-configuration generator found, ignoring all but first configuration";

    Configuration result = std::move(codemodel[0]);
    codemodel.clear();

    return result;
}

class PreprocessedData
{
public:
    CMakeProjectManager::CMakeConfig cache;

    QSet<FilePath> cmakeFiles;

    std::vector<std::unique_ptr<ProjectExplorer::FileNode>> cmakeNodesSource;
    std::vector<std::unique_ptr<ProjectExplorer::FileNode>> cmakeNodesBuild;
    std::vector<std::unique_ptr<ProjectExplorer::FileNode>> cmakeNodesOther;
    std::vector<std::unique_ptr<ProjectExplorer::FileNode>> cmakeListNodes;

    Configuration codemodel;
    std::vector<TargetDetails> targetDetails;
};

PreprocessedData preprocess(FileApiData &data,
                            const FilePath &sourceDirectory,
                            const FilePath &buildDirectory,
                            QString &errorMessage)
{
    PreprocessedData result;

    result.cache = std::move(data.cache); // Make sure this is available, even when nothing else is

    // Simplify to only one configuration:
    result.codemodel = extractConfiguration(data.codemodel, errorMessage);
    if (!errorMessage.isEmpty()) {
        return result;
    }

    CMakeFileResult cmakeFileResult = extractCMakeFilesData(data.cmakeFiles,
                                                            sourceDirectory,
                                                            buildDirectory);

    result.cmakeFiles = std::move(cmakeFileResult.cmakeFiles);
    result.cmakeNodesSource = std::move(cmakeFileResult.cmakeNodesSource);
    result.cmakeNodesBuild = std::move(cmakeFileResult.cmakeNodesBuild);
    result.cmakeNodesOther = std::move(cmakeFileResult.cmakeNodesOther);
    result.cmakeListNodes = std::move(cmakeFileResult.cmakeListNodes);

    result.targetDetails = std::move(data.targetDetails);

    return result;
}

QList<CMakeBuildTarget> generateBuildTargets(const PreprocessedData &input,
                                             const FilePath &sourceDirectory)
{
    QDir sourceDir(sourceDirectory.toString());
    const QList<CMakeBuildTarget> result = transform<
        QList>(input.targetDetails, [&sourceDir](const TargetDetails &t) -> CMakeBuildTarget {
        CMakeBuildTarget ct;
        ct.title = t.name;
        ct.executable = t.artifacts.isEmpty() ? FilePath() : t.artifacts.at(0);
        TargetType type = UtilityType;
        if (t.type == "EXECUTABLE")
            type = ExecutableType;
        else if (t.type == "STATIC_LIBRARY")
            type = StaticLibraryType;
        else if (t.type == "OBJECT_LIBRARY")
            type = ObjectLibraryType;
        else if (t.type == "MODULE_LIBRARY" || t.type == "SHARED_LIBRARY")
            type = DynamicLibraryType;
        else
            type = UtilityType;
        ct.targetType = type;
        if (t.artifacts.isEmpty()) {
            ct.workingDirectory = t.buildDir;
        } else {
            ct.workingDirectory = FilePath::fromString(QDir::cleanPath(
                QDir(t.buildDir.toString()).absoluteFilePath(t.artifacts.at(0).toString() + "/..")));
        }
        ct.sourceDirectory = FilePath::fromString(
            QDir::cleanPath(sourceDir.absoluteFilePath(t.sourceDir.toString())));
        return ct;
    });
    return result;
}

static QStringList splitFragments(const QStringList &fragments)
{
    QStringList result;
    for (const QString &f : fragments) {
        result += QtcProcess::splitArgs(f);
    }
    return result;
}

CppTools::RawProjectParts generateRawProjectParts(const PreprocessedData &input,
                                                  const FilePath &sourceDirectory)
{
    CppTools::RawProjectParts rpps;

    int counter = 0;
    for (const TargetDetails &t : input.targetDetails) {
        QDir sourceDir(sourceDirectory.toString());

        for (const CompileInfo &ci : t.compileGroups) {
            if (ci.language != "C" && ci.language != "CXX" && ci.language != "CUDA")
                continue; // No need to bother the C++ codemodel

            // CMake users worked around Creator's inability of listing header files by creating
            // custom targets with all the header files. This target breaks the code model, so
            // keep quiet about it:-)
            if (ci.defines.empty() && ci.includes.empty() && allOf(ci.sources, [t](const int sid) {
                    const SourceInfo &source = t.sources[static_cast<size_t>(sid)];
                    return Node::fileTypeForFileName(FilePath::fromString(source.path))
                           == FileType::Header;
                })) {
                qWarning() << "Not reporting all-header compilegroup of target" << t.name
                           << "to code model.";
                continue;
            }

            ++counter;
            CppTools::RawProjectPart rpp;
            rpp.setProjectFileLocation(t.sourceDir.pathAppended("CMakeLists.txt").toString());
            rpp.setBuildSystemTarget(t.name);
            rpp.setDisplayName(t.id);
            rpp.setMacros(transform<QVector>(ci.defines, &DefineInfo::define));
            rpp.setHeaderPaths(transform<QVector>(ci.includes, &IncludeInfo::path));

            CppTools::RawProjectPartFlags cProjectFlags;
            cProjectFlags.commandLineFlags = splitFragments(ci.fragments);
            rpp.setFlagsForC(cProjectFlags);

            CppTools::RawProjectPartFlags cxxProjectFlags;
            cxxProjectFlags.commandLineFlags = cProjectFlags.commandLineFlags;
            rpp.setFlagsForCxx(cxxProjectFlags);

            rpp.setFiles(transform<QList>(ci.sources, [&t, &sourceDir](const int si) {
                return sourceDir.absoluteFilePath(t.sources[static_cast<size_t>(si)].path);
            }));

            const bool isExecutable = t.type == "EXECUTABLE";
            rpp.setBuildTargetType(isExecutable ? CppTools::ProjectPart::Executable
                                                : CppTools::ProjectPart::Library);
            rpps.append(rpp);
        }
    }

    return rpps;
}

FilePath directorySourceDir(const Configuration &c, const QDir &sourceDir, int directoryIndex)
{
    const size_t di = static_cast<size_t>(directoryIndex);
    QTC_ASSERT(di < c.directories.size(), return FilePath());

    return FilePath::fromString(
        QDir::cleanPath(sourceDir.absoluteFilePath(c.directories[di].sourcePath)));
}

FilePath directoryBuildDir(const Configuration &c, const QDir &buildDir, int directoryIndex)
{
    const size_t di = static_cast<size_t>(directoryIndex);
    QTC_ASSERT(di < c.directories.size(), return FilePath());

    return FilePath::fromString(
        QDir::cleanPath(buildDir.absoluteFilePath(c.directories[di].buildPath)));
}

void addProjects(const QHash<Utils::FilePath, ProjectNode *> &cmakeListsNodes,
                 const Configuration &config,
                 const QDir &sourceDir)
{
    for (const FileApiDetails::Project &p : config.projects) {
        if (p.parent == -1)
            continue; // Top-level project has already been covered
        FilePath dir = directorySourceDir(config, sourceDir, p.directories[0]);
        createProjectNode(cmakeListsNodes, dir, p.name);
    }
}

void addBacktraceInformation(FolderNode *node,
                             const BacktraceInfo &backtraces,
                             const QDir &sourceDir,
                             int backtraceIndex)
{
    QList<FolderNode::LocationInfo> info;
    // Set up a default target path:
    FilePath targetPath = node->filePath().pathAppended("CMakeLists.txt");
    while (backtraceIndex != -1) {
        const size_t bi = static_cast<size_t>(backtraceIndex);
        QTC_ASSERT(bi < backtraces.nodes.size(), break);
        const BacktraceNode &btNode = backtraces.nodes[bi];
        backtraceIndex = btNode.parent; // advance to next node

        const size_t fileIndex = static_cast<size_t>(btNode.file);
        QTC_ASSERT(fileIndex < backtraces.files.size(), break);
        const FilePath path = FilePath::fromString(
            sourceDir.absoluteFilePath(backtraces.files[fileIndex]));

        if (btNode.command < 0) {
            // No command, skip: The file itself is already covered:-)
            continue;
        }

        const size_t commandIndex = static_cast<size_t>(btNode.command);
        QTC_ASSERT(commandIndex < backtraces.commands.size(), break);

        const QString command = backtraces.commands[commandIndex];

        QString dn;
        if (path == targetPath) {
            if (btNode.line > 0) {
                dn = QCoreApplication::translate("CMakeProjectManager::Internal::FileApiReader",
                                                 "%1 in line %2")
                         .arg(command)
                         .arg(btNode.line);
            } else {
                dn = command;
            }
        } else {
            if (btNode.line > 0) {
                dn = QCoreApplication::translate("CMakeProjectManager::Internal::FileApiReader",
                                                 "%1 in %2:%3")
                         .arg(command)
                         .arg(path.toUserOutput())
                         .arg(btNode.line);
            } else {
                dn = QCoreApplication::translate("CMakeProjectManager::Internal::FileApiReader",
                                                 "%1 in %2")
                         .arg(command)
                         .arg(path.toUserOutput());
            }
        }
        info.append(FolderNode::LocationInfo(dn, path, btNode.line));
    }
    node->setLocationInfo(info);
}

QVector<FolderNode *> addSourceGroups(ProjectNode *targetRoot,
                                      const TargetDetails &td,
                                      const Utils::FileName &sourceDirectory)
{
    QVector<FolderNode *> sourceGroupNodes;
    if (td.sourceGroups.size() == 1) {
        sourceGroupNodes.append(
            targetRoot); // Only one source group, so do not bother to display any:-)
    } else {
        for (const QString &sg : td.sourceGroups) {
            if (sg.isEmpty()) {
                sourceGroupNodes.append(targetRoot);
            } else {
                auto sgNode = createCMakeVFolder(sourceDirectory,
                                                 Node::DefaultFolderPriority + 5,
                                                 sg);
                sgNode->setListInProject(false);

                sourceGroupNodes.append(sgNode.get());
                targetRoot->addNode(std::move(sgNode));
            }
        }
    }

    return sourceGroupNodes;
}

void addCompileGroups(ProjectNode *targetRoot,
                      const Utils::FilePath &topSourceDirectory,
                      const Utils::FilePath &sourceDirectory,
                      const Utils::FilePath &buildDirectory,
                      const TargetDetails &td,
                      QSet<FilePath> &knownHeaderNodes)
{
    const bool inSourceBuild = (sourceDirectory == buildDirectory);
    const QDir currentSourceDir(sourceDirectory.toString());

    std::vector<std::unique_ptr<FileNode>> toList;
    QSet<Utils::FilePath> alreadyListed;

    // Files already added by other configurations:
    targetRoot->forEachGenericNode(
        [&alreadyListed](const Node *n) { alreadyListed.insert(n->filePath()); });

    QVector<FolderNode *> sourceGroupNodes = addSourceGroups(targetRoot, td, sourceDirectory);
    const QDir topSourceDir(topSourceDirectory.toString());

    std::vector<std::unique_ptr<FileNode>> buildFileNodes;
    std::vector<std::unique_ptr<FileNode>> otherFileNodes;

    for (const SourceInfo &si : td.sources) {
        const FilePath sourcePath = FilePath::fromString(
            QDir::cleanPath(topSourceDir.absoluteFilePath(si.path)));

        // Filter out already known files:
        const int count = alreadyListed.count();
        alreadyListed.insert(sourcePath);
        if (count == alreadyListed.count())
            continue;

        // Create FileNodes from the file
        auto node = std::make_unique<FileNode>(sourcePath, Node::fileTypeForFileName(sourcePath));
        node->setIsGenerated(si.isGenerated);

        // Register headers:
        if (node->fileType() == FileType::Header)
            knownHeaderNodes.insert(node->filePath());

        // Where does the file node need to go?
        if (sourcePath.isChildOf(buildDirectory) && !inSourceBuild) {
            buildFileNodes.emplace_back(std::move(node));
        } else if (sourcePath.isChildOf(sourceDirectory)) {
            sourceGroupNodes[si.sourceGroup]->addNode(std::move(node));
        } else {
            otherFileNodes.emplace_back(std::move(node));
        }
    }

    addCMakeVFolder(targetRoot,
                    buildDirectory,
                    100,
                    QCoreApplication::translate("CMakeProjectManager::Internal::FileApi",
                                                "<Build Directory>"),
                    std::move(buildFileNodes));
    addCMakeVFolder(targetRoot,
                    Utils::FilePath(),
                    10,
                    QCoreApplication::translate("CMakeProjectManager::Internal::FileApi",
                                                "<Other Locations>"),
                    std::move(otherFileNodes));
}

void addTargets(const QHash<Utils::FilePath, ProjectExplorer::ProjectNode *> &cmakeListsNodes,
                const Configuration &config,
                const std::vector<TargetDetails> &targetDetails,
                const FilePath &topSourceDir,
                const QDir &sourceDir,
                const QDir &buildDir,
                QSet<FilePath> &knownHeaderNodes)
{
    for (const FileApiDetails::Target &t : config.targets) {
        const TargetDetails &td = Utils::findOrDefault(targetDetails,
                                                       Utils::equal(&TargetDetails::id, t.id));

        const FilePath dir = directorySourceDir(config, sourceDir, t.directory);

        CMakeTargetNode *tNode = createTargetNode(cmakeListsNodes, dir, t.name);
        QTC_ASSERT(tNode, continue);

        tNode->setTargetInformation(td.artifacts, td.type);
        tNode->setBuildDirectory(directoryBuildDir(config, buildDir, t.directory));

        addBacktraceInformation(tNode, td.backtraceGraph, sourceDir, td.backtrace);

        addCompileGroups(tNode, topSourceDir, dir, tNode->buildDirectory(), td, knownHeaderNodes);
    }
}

std::pair<std::unique_ptr<CMakeProjectNode>, QSet<FilePath>> generateRootProjectNode(
    PreprocessedData &data, const FilePath &sourceDirectory, const FilePath &buildDirectory)
{
    std::pair<std::unique_ptr<CMakeProjectNode>, QSet<FilePath>> result;
    result.first = std::make_unique<CMakeProjectNode>(sourceDirectory);

    const QDir sourceDir(sourceDirectory.toString());
    const QDir buildDir(buildDirectory.toString());

    const FileApiDetails::Project topLevelProject
        = findOrDefault(data.codemodel.projects, equal(&FileApiDetails::Project::parent, -1));
    if (!topLevelProject.name.isEmpty())
        result.first->setDisplayName(topLevelProject.name);

    QHash<FilePath, ProjectNode *> cmakeListsNodes = addCMakeLists(result.first.get(),
                                                                   std::move(data.cmakeListNodes));
    data.cmakeListNodes.clear(); // Remove all the nullptr in the vector...

    QSet<FilePath> knownHeaders;
    addProjects(cmakeListsNodes, data.codemodel, sourceDir);

    addTargets(cmakeListsNodes,
               data.codemodel,
               data.targetDetails,
               sourceDirectory,
               sourceDir,
               buildDir,
               knownHeaders);

    // addHeaderNodes(root.get(), knownHeaders, allFiles);

    if (!data.cmakeNodesSource.empty() || !data.cmakeNodesBuild.empty()
        || !data.cmakeNodesOther.empty())
        addCMakeInputs(result.first.get(),
                       sourceDirectory,
                       buildDirectory,
                       std::move(data.cmakeNodesSource),
                       std::move(data.cmakeNodesBuild),
                       std::move(data.cmakeNodesOther));

    data.cmakeNodesSource.clear(); // Remove all the nullptr in the vector...
    data.cmakeNodesBuild.clear();  // Remove all the nullptr in the vector...
    data.cmakeNodesOther.clear();  // Remove all the nullptr in the vector...

    result.second = knownHeaders;

    return result;
}

} // namespace

namespace CMakeProjectManager {
namespace Internal {

using namespace FileApiDetails;

// --------------------------------------------------------------------
// extractData:
// --------------------------------------------------------------------

FileApiQtcData extractData(FileApiData &input,
                           const FilePath &sourceDirectory,
                           const FilePath &buildDirectory)
{
    FileApiQtcData result;

    // Preprocess our input:
    PreprocessedData data = preprocess(input, sourceDirectory, buildDirectory, result.errorMessage);
    result.cache = std::move(data.cache); // Make sure this is available, even when nothing else is
    if (!result.errorMessage.isEmpty()) {
        return {};
    }

    result.buildTargets = generateBuildTargets(data, sourceDirectory);
    result.cmakeFiles = std::move(data.cmakeFiles);
    result.projectParts = generateRawProjectParts(data, sourceDirectory);

    auto pair = generateRootProjectNode(data, sourceDirectory, buildDirectory);
    result.rootProjectNode = std::move(pair.first);
    result.knownHeaders = std::move(pair.second);

    return result;
}

} // namespace Internal
} // namespace CMakeProjectManager
